package com.qf.index;

import cn.hutool.core.collection.ConcurrentHashSet;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.db.Db;
import cn.hutool.db.Entity;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSONObject;

import java.sql.SQLException;
import java.text.Normalizer;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 将基因、疾病、通路、药物四种类型的词组，从多个来源表中读出，形成wordAndCodeSetMap，并保存到表wordAndCodeSetMapTable中。
 * 使用时，先读取表wordAndCodeSetMapTable，把wordAndCodeSetMap拿到，再执行getDrug方法获取。
 * 贾新志 2024.11.14
 */

public class Drug {

    public static void main(String[] args) throws SQLException {

        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        String limit = "";
//        String limit = " limit 1";
        String newDataName = "scholar_25_02.";
        String wordAndCodeSetMapTable = newDataName + "word_and_code_set_map";
        String drugBankTable = newDataName + "help_drugbank"; // 药物，数字代号：1
        String geneTable = newDataName + "help_genen"; // 基因，数字代号：2
        String pathwayTable = newDataName + "help_pathway"; // 通路，数字代号：3
        String navDiseaseTable = newDataName + "help_nav_disease"; // 疾病，数字代号：4
        String icd11Table = newDataName + "help_icd11_to_icd1020230417"; // 疾病，数字代号：4
        String icd11NewTable = newDataName + "help_icd11_newst"; // 疾病，数字代号：4
        String meshTable = newDataName + "help_mesh_tree_gcr_0615";
        String gene24Table = newDataName + "help_gene_240329";

        // saveWordAndCodeSetMap是提前把wordAndCodeSetMap的数据保存到表中，这里面需要操作大量的表。
        saveWordAndCodeSetMap(wordAndCodeSetMapTable, drugBankTable, geneTable, pathwayTable, navDiseaseTable, icd11Table, icd11NewTable, meshTable, gene24Table, limit);

        Map<String, Set<Integer>> wordAndCodeSetMap = getWordAndCodeSetMap(wordAndCodeSetMapTable, "id", "word`,`code_set", limit);
        System.out.println(format.format(new Date()) + " wordAndCodeSetMap.size():" + wordAndCodeSetMap.size());
//
//        // 实际测试
        String title = "Pegfilgrastim use and bone pain: a cohort study of community-based cancer patients Erythrocyte Membrane Band 3 Protein";
        String words = "Granulocyte colony-stimulating factor; neoplasms; neutropenia; antineoplastic combined chemotherapy protocols; pain";
        String abs = "Purpose Bone pain is a common adverse effect of the granulocyte colony-stimulating factors filgrastim and pegfilgrastim. However, the incidence of reported bone pain varies and therapies to mitigate this adverse effect are limited to case reports and one randomized controlled trial. The purpose of this study was to describe pegfilgrastim use, the incidence and treatment of bone pain, and rate of severe or febrile neutropenia among cancer patients receiving pegfilgrastim at a metropolitan, hospital-based, community cancer center. Methods This retrospective chart review included the first 100 adult oncology patients who received at least one dose of pegfilgrastim from 1 January 2012 to 31 December 2012. Descriptive analyses were used to evaluate the primary and secondary outcomes. Results Of the identified cases, 69 cancer patients were evaluable. Most patients (74%) received pegfilgrastim for primary prophylaxis. Pegfilgrastim-associated bone pain occurred in 19% and loratadine was the most common medication used to treat it. Among the patients who received pegfilgrastim for primary prophylaxis, 8% were hospitalized for febrile neutropenia. Among those hospitalized for febrile neutropenia, 64% had not received pegfilgrastim for primary prophylaxis. Conclusions Pegfilgrastim is commonly used for primary prophylaxis during the first cycle of chemotherapy. Hospitalizations for febrile neutropenia occurred most commonly among patients without primary prophylaxis. Pegfilgrastim-associated bone pain occurred in a similar percentage, as reported in randomized controlled trials but less than that reported by survey. Loratadine was the most commonly employed medication to mitigate this adverse effect.";
        List<En.CategoryAndInfo> drug = getDrug(title, words, abs, wordAndCodeSetMap);
        for (En.CategoryAndInfo categoryAndInfo : drug) {
            System.out.println(categoryAndInfo);
        }
        System.out.println(format.format(new Date()) + " drug.size():" + drug.size());

    }

    public static List<En.CategoryAndInfo> getDrug(String title, String words, String abs, Map<String, Set<Integer>> wordAndCodeSetMap) {
        Map<Integer, Set<String>> tiCodeAndWordSetMap = new HashMap<>();
        Map<Integer, Set<String>> kyCodeAndWordSetMap = new HashMap<>();
        Map<Integer, Set<String>> abCodeAndWordSetMap = new HashMap<>();
        getCodeAndWordSet(title, wordAndCodeSetMap, tiCodeAndWordSetMap);
        String[] kySplit = words.split("; ");
        for (String caseSensitiveStrings : kySplit) {
            // 关键词是有大小写的，而保存在wordAndCodeSetMap中的，都是小写，因此，判断时用小写，保存进codeAndWordSetMap时仍用原来的，确保了大小写的不变动。
            if (wordAndCodeSetMap.containsKey(caseSensitiveStrings.toLowerCase())) {
                Set<Integer> numberCodeSet = wordAndCodeSetMap.get(caseSensitiveStrings.toLowerCase());
                putCodeAndWordSetMap(caseSensitiveStrings, numberCodeSet, kyCodeAndWordSetMap);
            }
        }
        String[] abSplit = abs.split("\\. ");
        for (String sentence : abSplit) {
            getCodeAndWordSet(sentence, wordAndCodeSetMap, abCodeAndWordSetMap);
        }
        List<En.CategoryAndInfo> categoryAndInfoList = new ArrayList<>();
        categoryAndInfoList.add(addCategory(tiCodeAndWordSetMap, 7)); // 标题ti为7
        categoryAndInfoList.add(addCategory(kyCodeAndWordSetMap, 8)); // 关键词ky为8
        categoryAndInfoList.add(addCategory(abCodeAndWordSetMap, 9)); // 摘要ab为9
        return categoryAndInfoList;
    }


    public static Map<String, Set<Integer>> getWordAndCodeSetMap(String wordAndCodeSetMapTable, String autoId, String field, String limit) throws SQLException {
        // 此方法是先使用saveWordAndCodeSetMap把wordAndCodeSetMap存表，再给读出来，用来节省时间。
        int onePageNum = 1000;
        int tableMax = Db.use().queryNumber("select max(" + autoId + ") from " + wordAndCodeSetMapTable).intValue();
        Map<String, Set<Integer>> wordAndCodeSetMap = new ConcurrentHashMap<>();
        if (tableMax > 0) {
            Collection<Integer> tableList = new ArrayList<>();
            NumberUtil.appendRange(0, tableMax + onePageNum, onePageNum, tableList);
            tableList.parallelStream().forEach(startId -> {
                try {
                    List<Entity> tableData = Db.use().query("select `" + autoId + "`,`" + field + "` from " + wordAndCodeSetMapTable + " where " + autoId + " between " + startId + " and " + (startId + onePageNum - 1) + limit);
                    for (Entity entry : tableData) {
                        String word = entry.getStr("word");
                        String codeSet = entry.getStr("code_set");
                        if (null != word && null != codeSet && word.length() > 1 && codeSet.contains("]")) {
                            List<Integer> codeList = JSONObject.parseArray(codeSet, Integer.class);
                            wordAndCodeSetMap.put(word, new HashSet<>(codeList));
                        }
                    }
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            });
        }
        return wordAndCodeSetMap;
    }

    public static void saveWordAndCodeSetMap(String wordAndCodeSetMapTable, String drugBankTable, String geneTable, String pathwayTable, String navDiseaseTable, String icd11Table, String icd11NewTable, String meshTable, String gene24Table, String limit) throws SQLException {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println(format.format(new Date()) + " 程序开始了~~~");
        // 获取基因的名字列表geneMap，键是小写，值是对应的标准写法。
        Map<String, String> geneMap = getGeneSet(gene24Table, "id", "symbol`,`description");

        int onePageNum = 1000;
        Map<String, Set<Integer>> wordAndCodeSetMap = new ConcurrentHashMap<>();

        readDrugTable(drugBankTable, onePageNum, limit, "id", "generic_name_list`,`generic_name`,`generic_name_list", "drugBank", wordAndCodeSetMap, geneMap);
        System.out.println(format.format(new Date()) + " " + drugBankTable + " wordAndCodeSetMap.size(): " + wordAndCodeSetMap.size());

        readDrugTable(geneTable, onePageNum, limit, "id", "Other_designations`,`description", "gene", wordAndCodeSetMap, geneMap);
        System.out.println(format.format(new Date()) + " " + geneTable + " wordAndCodeSetMap.size(): " + wordAndCodeSetMap.size());

        readDrugTable(pathwayTable, onePageNum, limit, "id", "childOneName`,`childTwoName`,`childThreeName", "pathway", wordAndCodeSetMap, geneMap);
        System.out.println(format.format(new Date()) + " " + pathwayTable + " wordAndCodeSetMap.size(): " + wordAndCodeSetMap.size());

        readDrugTable(meshTable, onePageNum, limit, "id", "Subheadings`,`meshName`,`entryTerms", "mesh", wordAndCodeSetMap, geneMap);
        System.out.println(format.format(new Date()) + " " + meshTable + " wordAndCodeSetMap.size(): " + wordAndCodeSetMap.size());

        readDrugTable(navDiseaseTable, onePageNum, limit, "id", "name_en", "nav_disease", wordAndCodeSetMap, geneMap);
        System.out.println(format.format(new Date()) + " " + navDiseaseTable + " wordAndCodeSetMap.size(): " + wordAndCodeSetMap.size());

        readDrugTable(icd11Table, onePageNum, limit, "id", "icd11_title`,`icd10_title", "icd11_to_icd1020230417", wordAndCodeSetMap, geneMap);
        System.out.println(format.format(new Date()) + " " + icd11Table + " wordAndCodeSetMap.size(): " + wordAndCodeSetMap.size());

        readDrugTable(icd11NewTable, onePageNum, limit, "id", "title_en_d", "icd11_newst", wordAndCodeSetMap, geneMap);
        System.out.println(format.format(new Date()) + " " + icd11NewTable + " wordAndCodeSetMap.size(): " + wordAndCodeSetMap.size());

        Db.use().execute("truncate table " + wordAndCodeSetMapTable);
        int sum = 0;
        for (Map.Entry<String, Set<Integer>> entry : wordAndCodeSetMap.entrySet()) {
            try {
                Db.use().insert(Entity.create(wordAndCodeSetMapTable).set("word", entry.getKey()).set("code_set", JSONUtil.toJsonStr(entry.getValue())));
                sum++;
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        System.out.println("wordAndCodeSetMap.size():" + wordAndCodeSetMap.size() + " 实际插表:" + sum);

    }

    public static Map<String, String> getGeneSet(String tableName, String autoId, String field) throws SQLException {
        // 读取基因的名字列表，返回基因名字的集合，字段：symbol`,`description，表名：gene_240329
        int onePageNum = 1000;
        Map<String, String> geneMap = new HashMap<>();
        int tableMax = Db.use().queryNumber("select max(" + autoId + ") from " + tableName).intValue();
        if (tableMax > 0) {
            Collection<Integer> tableList = new ArrayList<>();
            NumberUtil.appendRange(0, tableMax + onePageNum, onePageNum, tableList);
            for (Integer startId : tableList) {
                try {
                    List<Entity> tableData = Db.use().query("select `" + autoId + "`,`" + field + "` from " + tableName + " where " + autoId + " between " + startId + " and " + (startId + onePageNum - 1));
                    for (Entity entry : tableData) {
                        String symbol = entry.getStr("symbol");
                        String description = entry.getStr("description");
                        if (null != symbol && null != description && !symbol.isEmpty() && !description.isEmpty()) {
                            geneMap.put(description.toLowerCase().trim(), symbol);
                            geneMap.put(symbol.toLowerCase().trim(), symbol);
                        }
                    }
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
        return geneMap;
    }

    public static void readDrugTable(String tableName, int onePageNum, String limit, String autoId, String field, String type, Map<String, Set<Integer>> wordAndCodeSetMap, Map<String, String> geneMap) throws SQLException {
        int tableMax = Db.use().queryNumber("select max(" + autoId + ") from " + tableName).intValue();
        if (tableMax > 0) {
            Collection<Integer> tableList = new ArrayList<>();
            NumberUtil.appendRange(0, tableMax + onePageNum, onePageNum, tableList);
            for (Integer startId : tableList) {
                try {
                    List<Entity> tableData = Db.use().query("select `" + autoId + "`,`" + field + "` from " + tableName + " where " + autoId + " between " + startId + " and " + (startId + onePageNum - 1) + limit);
                    for (Entity entry : tableData) {
                        if ("mesh".equals(type)) {
                            String subheadings = entry.getStr("Subheadings");
                            if (null != subheadings && subheadings.length() > 10) {
                                subheadings = subheadings.toLowerCase();
                                String meshName = entry.getStr("meshName");
                                String entryTerms = entry.getStr("entryTerms");
                                Set<String> wordSet = new HashSet<>();
                                if (null != entryTerms && entryTerms.contains("]")) {
                                    List<String> entryTermsList = JSONObject.parseArray(entryTerms, String.class);
                                    wordSet.addAll(entryTermsList);
                                }
                                wordSet.add(meshName); // 这里并不没有判断null或者最低的长度，而是放在了下面。
                                // 如果副主题词含有药物治疗、病因、死亡率，一般对应的词，应该是疾病。疾病，数字代号：4
                                if (subheadings.contains("drug therapy") || subheadings.contains("etiology") || subheadings.contains("mortality")) {
                                    for (String word : wordSet) {
//                                        putWordAndCodeSetMap(word, 4, wordAndCodeSetMap);
                                    }
                                }
                                // 如果副主题词含有给药和剂量、药代动力学、治疗应用，一般对应的词，应该是药物。药物，数字代号：1
                                if (subheadings.contains("administration and dosage") || subheadings.contains("pharmacokinetics") || subheadings.contains("therapeutic use")) {
                                    for (String word : wordSet) {
//                                        putWordAndCodeSetMap(word, 1, wordAndCodeSetMap);
                                    }
                                }
                            }
                        }
                        if ("drugBank".equals(type)) {
                            String genericNameList = entry.getStr("generic_name_list");
                            Set<String> wordSet = new HashSet<>();
                            if (null != genericNameList && genericNameList.contains("]")) {
                                List<String> list = JSONObject.parseArray(genericNameList, String.class);
                                wordSet.addAll(list);
                            }
                            wordSet.add(entry.getStr("generic_name"));
                            for (String word : wordSet) {
                                putWordAndCodeSetMap(word, 1, wordAndCodeSetMap);
                            }
                        }
                        if ("gene".equals(type)) { // 基因，数字代号：2
                            // 字段otherDesignations，当有多个时，内部是用"|"进行分隔的，因此，也可以先把description给加进去，再切割。
                            String otherDesignations = entry.getStr("Other_designations");
                            String description = entry.getStr("description");
                            String result = ((null == description) ? "" : description + "|") + ((null == otherDesignations) ? "" : otherDesignations);
                            String[] split = result.split("\\|");
                            for (String word : split) {
                                String lower = word.toLowerCase().trim();
                                // 2024.11.12 增加了过滤。
                                if (geneMap.containsKey(lower)) {
                                    putWordAndCodeSetMap(geneMap.get(lower), 2, wordAndCodeSetMap);
                                }
                            }
                        }
                        if ("pathway".equals(type)) { // 通路，数字代号：3
                            Set<String> wordSet = new HashSet<>();
                            wordSet.add(entry.getStr("childOneName"));
                            wordSet.add(entry.getStr("childTwoName"));
                            wordSet.add(entry.getStr("childThreeName"));
                            for (String word : wordSet) {
                                putWordAndCodeSetMap(word, 3, wordAndCodeSetMap);
                            }
                        }
                        if ("icd11_to_icd1020230417".equals(type)) { // 疾病，数字代号：4
                            putWordAndCodeSetMap(entry.getStr("icd11_title"), 4, wordAndCodeSetMap);
                            putWordAndCodeSetMap(entry.getStr("icd10_title"), 4, wordAndCodeSetMap);
                        }
                        if ("icd11_newst".equals(type)) { // 疾病，数字代号：4
                            putWordAndCodeSetMap(entry.getStr("title_en_d"), 4, wordAndCodeSetMap);
                        }
                        if ("nav_disease".equals(type)) { // 疾病，数字代号：4
                            putWordAndCodeSetMap(entry.getStr("name_en"), 4, wordAndCodeSetMap);
                        }
                    }
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 参数codeAndWordSetMap：数字代号和关键词的集合；category：类别，共分为三种：标题ti为7、关键词ky为8、摘要ab为9。功能：对数字代号和关键词集合转换为实体类（为什么转换？因为Map具用去重复功能，而实体类没有，所以运行程序时，使用Map，而保存时使用实体类），再根据指定的类别进行保存后返回。
    public static En.CategoryAndInfo addCategory(Map<Integer, Set<String>> codeAndWordSetMap, int category) {
        List<En.CodeAndWordList> infoList = new ArrayList<>();
        for (Map.Entry<Integer, Set<String>> entry : codeAndWordSetMap.entrySet()) {
            En.CodeAndWordList codeAndNameList = new En.CodeAndWordList();
            codeAndNameList.setCode(entry.getKey());
            codeAndNameList.setWordList(new ArrayList<>(entry.getValue()));
            infoList.add(codeAndNameList);
        }
        En.CategoryAndInfo categoryAndInfo = new En.CategoryAndInfo();
        categoryAndInfo.setInfoList(infoList);
        categoryAndInfo.setCategory(category);
        return categoryAndInfo;
    }

    // 参数sentence：标题、关键词或摘要中的句子；wordAndCodeSetMap：关键词和数字代号的集合；codeAndWordSetMap：数字代号和关键词的集合。功能：两头卡的方案提取关键词，此方法以空格进行切割再从前和从后进行卡，当无空格时的独立词也可以匹配的上。注意事项：匹配前，需要对sentence进行一次通用的字符串处理，因为加入时都作了。
    public static void getCodeAndWordSet(String sentence, Map<String, Set<Integer>> wordAndCodeSetMap, Map<Integer, Set<String>> codeAndWordSetMap) {
        if (null != wordAndCodeSetMap && !wordAndCodeSetMap.isEmpty() && null != codeAndWordSetMap) {
            sentence = changeString(generic(sentence));
            if (sentence.length() > 2) {
                String[] split = sentence.toLowerCase().split(" ");
                int len = 0;
                for (String s : split) {
                    String sub = sentence.toLowerCase().substring(len);
                    String[] split1 = sub.split(" ");
                    int len1 = sub.length();
                    for (int j = split1.length - 1; j >= 0; j--) {
                        String aub1 = sub.substring(0, len1);
                        if (aub1.length() > 1 && wordAndCodeSetMap.containsKey(aub1)) {
                            Set<Integer> numberCodeSet = wordAndCodeSetMap.get(aub1);
                            // 保持原字符串的大小写状态。如何实现的呢？原来的str是有大小写区分的，转split时转为小写了，但大小写的长度等信息是一致，当有匹配的到之后，再使用substring方法取原来的有大小写的str即可。
                            if ((len + aub1.length()) >= len && sentence.length() >= len && sentence.length() >= (len + aub1.length())) {
                                try {
                                    String caseSensitiveStrings = sentence.substring(len, len + aub1.length());
                                    putCodeAndWordSetMap(caseSensitiveStrings, numberCodeSet, codeAndWordSetMap);
                                } catch (Exception e) {
                                    throw new RuntimeException(e);
                                }
                            }
                        }
                        len1 = len1 - split1[j].length() - 1;
                    }
                    len += 1 + s.length();
                }
            }
        }
    }

    // 实践中发现String sentence = "in İzmir Katip";，其sentence.length()与sentence.toLowerCase().length()不一致，原因是编码不一致导致的，可以使用Unicode规范化函数来处理，但规范化后会凭空多出来一些字符，因此可以使用本方法来处理，处理后不改变大小写，且长度是一致的。
    public static String changeString(String sentence) {
        if (null == sentence || sentence.isEmpty()) {
            return "";
        }
        sentence = sentence.replace("İ", "I");
        if (sentence.length() == sentence.toLowerCase().length()) {
            return sentence;
        }
        String[] split = sentence.split("");
        StringBuilder result = new StringBuilder();
        for (String s : split) {
            if (s.toLowerCase().length() == 1) {
                result.append(s);
            } else {
                String lowerCase = s.toLowerCase();
                // 要保持转换为小写后字符串的长度不变，可以使用Unicode规范化函数来处理特殊字符，处理后字符中的组合字符分解为基本字符和组合字符，再将组合字符给予删除。
                String normalized = Normalizer.normalize(lowerCase, Normalizer.Form.NFD);
                result.append(normalized.replaceAll("\\p{InCombiningDiacriticalMarks}+", "").trim());
            }
        }
        return result.toString();
    }

    // 原则：1.把中文的一些常见符号，转为英文；2.引号、括号、斜杠、冒号、问号、感叹号、分号等转换成空格；3.多余的空格给予删除，包括"["之后的空格，"]"之前的空格；","、"."、"'"、"-"等前后的空格，在执行删除空格之前，需先把多个空格替换为一个空格；4.如果最后一位是英文的句号，给予删除；5.如果是null，直接返回空字符串。
    public static String generic(String str) {
        if (null == str) {
            return "";
        }
        str = str.replace("，", ",").replace("。", ".").replace("？", "?").replace("！", "!").replace("；", ";").replace("“", " ").replace("”", " ")
                .replace("\"", " ").replace("（", " ").replace("）", " ").replace("：", " ").replace("(", " ").replace(")", " ").replace(":", " ")
                .replace("?", " ").replace("!", " ").replace("\\", " ").replace("/", " ").replace(";", " ").replaceAll(" +", " ").replace("[ ", "[")
                .replace(" ]", "]").replace(" ,", ",").replace(", ", ",").replace(" .", ".").replace(". ", ".").replace(" '", "'").replace("' ", "'")
                .replace(" -", "-").replace("- ", "-").replaceAll(" +", " ").trim();
        str = str.endsWith(".") ? str.substring(0, str.length() - ".".length()).trim() : str;
        return str;
    }

    // 参数word：待加入的关键词；numberCode：数字代号；wordAndCodeSetMap：关键词和数字代号的集合。功能：对待加入的关键词，先作一次通用的字符串处理并转小写，处理后如果大于3位，则put进wordAndCodeSetMap。注意事项：put时作了相应的处理，get时也要作相应的处理。
    public static void putWordAndCodeSetMap(String word, int numberCode, Map<String, Set<Integer>> wordAndCodeSetMap) {
        word = generic(word).toLowerCase();
        // 2025.03.29 发现单词"hospital"属于疾病，特给予排除。
        if (word.length() > 3 && numberCode > 0 && !word.equals("hospital")) {
            wordAndCodeSetMap.compute(word, (k, v) -> {
                if (v == null) {
                    v = new ConcurrentHashSet<>();
                }
                v.add(numberCode);
                return v;
            });
        }
    }

    // 参数caseSensitiveStrings：区分大小写的字符串；numberCodeSet：数字代号的集合；codeAndWordSetMap：数字代号和关键词的集合。功能：如果区分大小写的字符串不为null且大于3位，数字代号大于0，则put进数字代号和关键词的集合。
    public static void putCodeAndWordSetMap(String caseSensitiveStrings, Set<Integer> numberCodeSet, Map<Integer, Set<String>> codeAndWordSetMap) {
        if (null != caseSensitiveStrings && caseSensitiveStrings.length() > 3) {
            for (Integer numberCode : numberCodeSet) {
                if (null != numberCode && numberCode > 0) {
                    codeAndWordSetMap.compute(numberCode, (k, v) -> {
                        if (v == null) {
                            v = new ConcurrentHashSet<>();
                        }
                        v.add(caseSensitiveStrings);
                        return v;
                    });
                }
            }
        }
    }

    /*
      CREATE TABLE `word_and_code_set_map` (
        `id` int(9) NOT NULL AUTO_INCREMENT,
        `word` text,
        `code_set` text,
        PRIMARY KEY (`id`)
      ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
     */


}
